﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
using Amazon.S3.Transfer;
using CoreLibrary.Aws;
using CoreLibrary.Service;

namespace CoreLibrary.Storage {

    public class AmazonS3StorageService : ServiceBase, IStorageService {

        public string BucketName { get; set; }

        public AwsAccount AccessAccount { get; set; }
        public int MinMultiPartUploadSize { get; set; }
        private static readonly int Mb2 = (int) Math.Pow(2.0, 20.0);

        public AmazonS3StorageService() {
           MinMultiPartUploadSize = 3;
        }

        protected override void OnInit() {
            Asserts.NotNull (AccessAccount, "AwsAccount is not provided.");
            Asserts.NotBlank(BucketName,    "BucketName is not specified");
        }

        protected override void OnStart() {
            GetAmazonS3();
        }

        protected override void OnStop() {
            if (GetAmazonS3() != null) {
                GetAmazonS3().Dispose();
            }
        }

        protected override void OnDestroy() {
            Stop();  
        }
        
        public StorageItem GetItem(string path) {
            try {
                var request = new GetObjectRequest()
                            .WithBucketName(BucketName)
                            .WithKey(S3Util.NormalizePath(path));

                using (var response = GetAmazonS3().GetObject(request)) {
                    return new StorageItem {
                           Data = IOUtil.Read(response.ResponseStream),
                           Metadata   = new StorageItemMetadata {
                               Author = response.Metadata["x-amz-meta-author"],
                               Key    = Util.GetString(path),
                               LastModified = S3Util.GetDateModified(response)
                           }
                    };
                }
            }
            catch (AmazonS3Exception exception) {
                if (exception.StatusCode == System.Net.HttpStatusCode.NotFound
                 || exception.ErrorCode.Equals("NoSuchKey")) {
                    return null;
                }
                throw new Exception(
                      string.Format("ErrorCode '{0}', Message '{1}', HttpStatusCode '{2}'", 
                             exception.ErrorCode, 
                             exception.Message,
                             exception.StatusCode
                      )
                 );
            }
        }

        public bool TryGetItem(string path, out StorageItem item) {
            try {
                item = GetItem(path);
                return item != null;
            } catch {
                item = null;
                return false;
            }
        }

        public bool TryPutItem(StorageItem item) {
            try {
                PutItem(item);
                return true;
            } 
            catch {
                return false;
            }
        }

        public void PutItem(StorageItem item) {
            try {
                var config = new TransferUtilityConfig {
                    MinSizeBeforePartUpload = MinMultiPartUploadSize * Mb2,
                };

                using (var transfer = new TransferUtility(GetAmazonS3(), config)) {
                       var request  = new TransferUtilityUploadRequest();
                       request.WithBucketName(BucketName);
                       request.WithInputStream(new MemoryStream(item.Data));
                       request.WithCannedACL(S3CannedACL.AuthenticatedRead);
                       request.WithKey(S3Util.NormalizePath(item.Metadata.Key));
                       transfer.Upload(request);
                }
            } 
            catch (AmazonS3Exception exception) {
                throw new Exception(
                      string.Format("ErrorCode '{0}', Message '{1}', HttpStatusCode '{2}'",
                          exception.ErrorCode,
                          exception.Message,
                          exception.StatusCode
                      )
                 );
            }
        }
        
        public void CopyItem(StorageItem item, string targetPath) {
            try {
                var request = new CopyObjectRequest();
                request.WithSourceBucket(BucketName)
                       .WithDestinationBucket(BucketName)
                       .WithSourceKey(item.Metadata.Key)
                       .WithDestinationKey(targetPath)
                       .WithCannedACL(S3CannedACL.AuthenticatedRead);

                using (GetAmazonS3().CopyObject(request)) {}
            } 
            catch (AmazonS3Exception exception) {
                throw new Exception(
                      string.Format("ErrorCode '{0}', Message '{1}', HttpStatusCode '{2}'",
                          exception.ErrorCode,
                          exception.Message,
                          exception.StatusCode
                      )
                 );
            }
        }

        public bool TryCopyItem(StorageItem item, string targetPath) {
            try {
                CopyItem(item, targetPath);
                return true;
            }
            catch {
                return false;
            }
        }

        public void RemoveItems(IEnumerable<string> itemPaths) {
            if (itemPaths == null) {
                throw new ArgumentException("items can not be empty");
            }

            try {
                var request = new DeleteObjectsRequest()
                             .WithBucketName(BucketName)
                             .WithKeys(itemPaths.Select(path => new KeyVersion(path)).ToArray())
                ;
                using (GetAmazonS3().DeleteObjects(request)) {}
            } 
            catch (DeleteObjectsException exception) {
                throw new Exception(
                      string.Format("ErrorCode '{0}', Message '{1}', HttpStatusCode '{2}'",
                          exception.ErrorCode,
                          exception.Message,
                          exception.StatusCode
                      )
                 );
            } 
            catch (Exception exception) {
                throw new Exception("Delete objects failed - " + exception.Message, exception);
            }
        }

        public bool TryRemoveItems(IEnumerable<string> itemPaths) {
            try {
                RemoveItems(itemPaths);
                return true;
            }
            catch {
                return false;
            }
        }

        private AmazonS3 _amazonS3;

        private AmazonS3 GetAmazonS3() {
            if (_amazonS3 == null) {
                lock (this) {
                    if (_amazonS3 == null) {
                        _amazonS3 = new AmazonS3Client(
                            new BasicAWSCredentials(AccessAccount.AccessKey, AccessAccount.SecretKey),
                            new AmazonS3Config {
                                CommunicationProtocol = Protocol.HTTP
                            }
                        );
                        using (var response = _amazonS3.ListBuckets()) {
                            if (!response.Buckets.Any(bucket => bucket.BucketName.Equals(BucketName))) {
                                var request = new PutBucketRequest {
                                    BucketName = BucketName
                                };
                                using (_amazonS3.PutBucket(request)){}
                            }
                        }
                    }
                }
            }
            return _amazonS3;
        }
    }
}